﻿using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
using System.Collections;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.FindSymbols;
using SharpAnalyzer.Analyzers;
using SharpAnalyzer.Collectors;
using SharpAnalyzer.Entities;
using SharpAnalyzer.Utilities;

namespace SharpAnalyzer.Calculators
{
    internal sealed class TypeMetricsCalculator
    {
        private static readonly IEnumerable<TypeKind> inheritableTypes = new TypeKind[2]
        {
            TypeKind.Class,
            TypeKind.Struct
        };

        private SemanticModel semanticModel;
        private Compilation compilation;
        private Solution solution;

        public TypeMetricsCalculator(Solution solution, Compilation compilation)
        {
            Verify.NotNull(() => compilation);
            Verify.NotNull(() => solution);

            this.compilation = compilation;
            this.solution = solution;
        }

        // a good place to look for metrics meanings http://www.ndepend.com/docs/code-metrics
        public void Calculate(ref Entities.Type typeMetric)
        {
            if (!typeMetric.SyntaxNodes.Any())
                return;

            // TODO: (typeMetric.SyntaxNodes > 1) >>>> Partial Class
            CodeSectionDeclarationSyntaxInfo typeNode = typeMetric.SyntaxNodes.FirstOrDefault();
            semanticModel = compilation.GetSemanticModel(typeNode.Syntax.SyntaxTree);
            TypeDeclarationSyntax type = (TypeDeclarationSyntax)typeNode.Syntax;

            IEnumerable<Member> memberMetrics = CalculateMemberMetrics(typeMetric);

            // Calculate Internal Metrics
            IEnumerable<string> sourceCC = CalculateClassCoupling(type, memberMetrics);
            LcomMetric lcomMetrics = new LackOfCohesionAnalyzer(semanticModel).Calculate(type);
            int afferentCoupling = CalculateAfferentCoupling(type);
            var efferentCoupling = GetEfferentCoupling(type);
            // Using Metrics to Find Out if Your Code Base Will Stand the Test of Time
            // http://www.informit.com/articles/article.aspx?p=1561879&seqNum=3
            double instability = Math.Round((double)efferentCoupling / (efferentCoupling + afferentCoupling), 2);
            typeMetric.Name = TypeNameResolver.GetName(type);
            typeMetric.Kind = GetMetricKind(type);
            typeMetric.Members = memberMetrics.ToList();
            typeMetric.Metrics = new List<Metric>()
            {
                new Metric()
                {
                    Name = MetricNames.MaintainabilityIndex,
                    Value = CalculateAveMaintainabilityIndex(memberMetrics),
                },
                new Metric()
                {
                    Name = MetricNames.CyclomaticComplexity,
                    Value = CalculateCyclomaticComplexity(memberMetrics),
                },
                new ClassCouplingMetricResult()
                {
                    Name = MetricNames.ClassCoupling,
                    Value = sourceCC.Count(),
                    Types = sourceCC,
                },
                new Metric()
                {
                    Name = MetricNames.DepthOfInheritance,
                    Value = CalculateDepthOfInheritance(type),
                },
                new Metric()
                {
                    Name = MetricNames.LinesOfCode,
                    Value = CalculateLinesOfCode(memberMetrics)
                },
                new Metric()
                {
                    Name = MetricNames.LackOfCohesionOfMethods,
                    Value = lcomMetrics.Lcom
                },
                new Metric()
                {
                    Name = MetricNames.LackOfCohesionOfMethodsHendersonSellers,
                    Value = lcomMetrics.LcomHendersonSellers
                },
                new Metric()
                {
                    Name = MetricNames.NumberOfFields,
                    Value = lcomMetrics.NumFields
                },
                new Metric()
                {
                    Name = MetricNames.NumberOfMethods,
                    Value = lcomMetrics.NumMethods
                },
                new Metric()
                {
                    Name = MetricNames.AfferentCoupling,
                    Value = afferentCoupling
                },
                new Metric()
                {
                    Name = MetricNames.EfferentCoupling,
                    Value = efferentCoupling
                },
                new Metric()
                {
                    Name = MetricNames.Instability,
                    Value = instability
                },
            };
        }

        private TypeMetricKind GetMetricKind(TypeDeclarationSyntax type)
        {
            if (type is ClassDeclarationSyntax)
                return TypeMetricKind.Class;
            if (type is InterfaceDeclarationSyntax)
                return TypeMetricKind.Interface;
            if (type is StructDeclarationSyntax)
                return TypeMetricKind.Struct;

            return TypeMetricKind.Unknown;
        }

        private IEnumerable<Member> CalculateMemberMetrics(Entities.Type typeMetric)
        {
            List<Member> retval = new List<Member>();

            foreach (var typeNode in typeMetric.SyntaxNodes)
            {
                semanticModel = compilation.GetSemanticModel(typeNode.Syntax.SyntaxTree);

                IEnumerable<Member> memberMetrics = new MemberCollectorSyntaxWalker().GetMembers(semanticModel, typeNode);
                MemberMetricsCalculator metricCalculator = new MemberMetricsCalculator(solution, semanticModel);

                for (int cnt = 0; cnt < memberMetrics.Count(); cnt++)
                {
                    var memberMetric = memberMetrics.ElementAt(cnt);

                    metricCalculator.Calculate(ref memberMetric);
                }
                retval.AddRange(memberMetrics);
            }


            return retval.AsReadOnly();
        }

        #region Accumulate Results
        private int CalculateCyclomaticComplexity(IEnumerable<Member> memberMetrics)
        {
            var source = from memberMetric in memberMetrics
                         from metric in memberMetric.Metrics
                         where metric.Name == MetricNames.CyclomaticComplexity
                         select (int)metric.Value;

            return source.Sum();
        }

        private int CalculateLinesOfCode(IEnumerable<Member> memberMetrics)
        {
            var source = from memberMetric in memberMetrics
                         from metric in memberMetric.Metrics
                         where metric.Name == MetricNames.LinesOfCode
                         select (int)metric.Value;

            return source.Sum();
        }

        private double CalculateAveMaintainabilityIndex(IEnumerable<Member> memberMetrics)
        {
            var source = from memberMetric in memberMetrics
                         from metric in memberMetric.Metrics
                         where metric.Name == MetricNames.MaintainabilityIndex
                         select (double)metric.Value;

            if (source.Any())
                return Math.Round(source.Average());
            return 100.0;
        }

        private IEnumerable<string> CalculateClassCoupling(TypeDeclarationSyntax type, IEnumerable<Member> memberMetrics)
        {
            IEnumerable<string> fromType = new TypeClassCouplingAnalyzer(semanticModel).Calculate(type);

            var fromMembers = from memberMetric in memberMetrics
                              from metric in memberMetric.Metrics.OfType<ClassCouplingMetricResult>()
                              from typeStr in metric.Types
                              select typeStr;
            var all = fromMembers.Concat(fromType);
            var retval = all.Distinct().OrderBy(x => x);
            return retval;
        }

        private int CalculateDepthOfInheritance(TypeDeclarationSyntax type)
        {
            int num = 0;
            INamedTypeSymbol symbolInfo = semanticModel.GetDeclaredSymbol(type);
            if (symbolInfo != null)
            {
                symbolInfo = symbolInfo.BaseType;
                while (symbolInfo != null)
                {
                    if (inheritableTypes.Any(x => x == symbolInfo.TypeKind))
                        checked { ++num; }
                    symbolInfo = symbolInfo.BaseType;
                }
            }
            return num;
        }

        private int CalculateAfferentCoupling(SyntaxNode node)
        {
            try
            {
                if (solution == null)
                    return 0;

                var symbol = semanticModel.GetDeclaredSymbol(node);
                if (symbol == null)
                    return 0;
                var symbolFinder = SymbolFinder.FindReferencesAsync(symbol, solution);
                symbolFinder.Wait();
                return symbolFinder.Result.SelectMany(x => x.Locations).Count();
            }
            catch
            {
                // Some constructors are not present in syntax tree because they have been created for metrics calculation.
                return 0;
            }
        }

        private int GetEfferentCoupling(SyntaxNode node)
        {
            var sourceSymbol = semanticModel.GetDeclaredSymbol(node);

            var typeSyntaxes = node.DescendantNodesAndSelf().OfType<TypeSyntax>();
            var commonSymbolInfos = typeSyntaxes.Select(x => semanticModel.GetSymbolInfo(x)).AsArray();
            var members = commonSymbolInfos
                .Select(x => x.Symbol)
                .Where(x => x != null)
                .Select(x =>
                {
                    var typeSymbol = x as ITypeSymbol;
                    return typeSymbol == null ? x.ContainingType : x;
                })
                .Cast<ITypeSymbol>()
                .WhereNotNull()
                .DistinctBy(x => x.ToDisplayString())
                .Count(x => !x.Equals(sourceSymbol));

            return members;
        }
        #endregion
    }
}
